package it.gtd.swimpool.model.entity;

import com.google.common.annotations.Beta;
import com.google.common.collect.ImmutableList;
import com.google.common.hash.Hashing;
import org.codehaus.jackson.annotate.JsonIgnore;

import javax.enterprise.inject.Default;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.google.common.base.Preconditions.*;

@Entity
@Table(name = "users")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "role", discriminatorType = DiscriminatorType.STRING, length = 5)
@DiscriminatorValue("user")
@NamedQueries({
    @NamedQuery(name = "User.byEmail", query = "SELECT user FROM User AS user WHERE user.email = :email"),
    @NamedQuery(name = "User.byName",  query = "SELECT user FROM User AS user WHERE user.name = :name"),
    @NamedQuery(name = "User.bySessionId", query = "SELECT user FROM User AS user WHERE user.sessionId = :session_id"),
    @NamedQuery(name = "User.inactive", query = "SELECT user FROM User AS user WHERE user.active = false")
})
@Beta
public class User implements Serializable {
    @Id
    @GeneratedValue
    private int id;

    @Column(nullable = false, unique = true)
    @javax.validation.constraints.Pattern(regexp = EMAIL_PATTERN, message = "Invalid email")
    @Size(max = 255)
    private String email;

    @Basic(optional = true)
    @Column(unique = true)
    @Size(max = 255)
    private String name;

    @Column(name = "password_hash", columnDefinition = "BINARY(64)", nullable = false)
    @NotNull
    @Size(min = 64, max = 64)
    @JsonIgnore
    private byte[] passwordHash;

    @Basic(fetch = FetchType.LAZY, optional = true)
    @Column(columnDefinition = "TEXT")
    private String description;

    @Basic(optional = true)
    @Size(max = 255)
    private String information;

    @Basic(optional = true)
    @Default
    @JsonIgnore
    @Column(columnDefinition = "BIT", length = 1)
    private boolean demoted;

    @Basic(optional = false)
    @Default
    @Column(columnDefinition = "BIT", length = 1)
    private boolean active;

    @ManyToMany
    @JoinTable(name = "friends", joinColumns = @JoinColumn(name = "users_id"))
    @JsonIgnore
    private List<User> friends;

    @ManyToMany
    @JoinTable(name = "user_skills", joinColumns = @JoinColumn(name = "users_id"))
    @JsonIgnore
    private List<Skill> skills;

    @Basic(fetch = FetchType.LAZY)
    @Lob
    @JsonIgnore
    @Column(columnDefinition = "MEDIUMBLOB")
    private byte[] avatar;

    @Basic
    @Column(name = "created_at")
    @Temporal(TemporalType.TIMESTAMP)
    @JsonIgnore
    private Date creationDate;

    @Column(name = "session_hash", nullable = true)
    private String sessionId;

    transient private Pattern emailPattern;

    transient private static final String EMAIL_PATTERN =
            "^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@"
                    + "[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,4})$";

    public User() {
        this.creationDate = new Date();

        if (emailPattern == null) {
            emailPattern = Pattern.compile(EMAIL_PATTERN);
        }
    }

    public User(String email) {
        this();

        checkNotNull(email, "email can't be null");

        this.setEmail(email);
        this.setName(email);
    }


    public int getId() {
        return id;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        checkNotNull(email, "email can't be null");
        Matcher matcher = emailPattern.matcher(email);
        checkArgument(matcher.matches(), "Invalid email string");

        this.email = email;
    }

    public String getName() {
        return name;
    }

    //Questo metodo dovrebbe essere friendly solamente a AdministrationManager, ma java manca di queste cose
    public void setName(String name) {
        checkNotNull(name, "name can't be null");
        checkArgument(name.length() <= 255, "Invalid name");

        this.name = name;
    }

    public boolean verifyPassword(String password) {
        checkNotNull(password, "password can't be null");

        return Arrays.equals(this.passwordHash, Hashing.sha512().hashString(password).asBytes());
    }

    public void setPassword(String password) {
        checkNotNull(password, "password can't be null");
        checkArgument(password.length() >= 6, "Password length must be greater then 6");

        this.passwordHash = Hashing.sha512().hashString(password).asBytes();
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        checkNotNull(description, "description can't be null");
        checkArgument(!description.isEmpty(), "Description can't be empty");

        this.description = description;
    }

    public String getInformation() {
        return information;
    }

    public void setInformation(String information) {
        this.information = information;
    }

    public boolean isDemoted() {
        return demoted;
    }

    public void demote() {
        this.demoted = true;
    }

    public Collection<Skill> getSkills() {
        return null != this.skills ?
                ImmutableList.<Skill>builder().addAll(this.skills).build() :
                ImmutableList.<Skill>builder().build();
    }

    public void addSkill(Skill skill) {
        checkNotNull(skill, "skill can't be null");

        if (this.skills == null) {
            this.skills = new ArrayList<Skill>();
        }

        this.skills.add(skill);
    }

    public void removeSkill(Skill skill) {
        if (this.skills != null) {
            this.skills.remove(skill);
        }
    }

    public Collection<User> getFriends() {
        return null != this.friends ?
                ImmutableList.<User>builder().addAll(this.friends).build() :
                ImmutableList.<User>builder().build();
    }

    public void addFriend(User friend) {
        checkNotNull(friend, "friend can't be null");

        if (this.friends == null) {
            this.friends = new LinkedList<User>();
        }

        this.friends.add(friend);
    }

    public void removeFriend(User friend) {
        checkNotNull(friend, "friend can't be null");

        if (this.friends != null) {
            this.friends.remove(friend);
        }
    }

    public byte[] getAvatar() {
        return avatar;
    }

    public void loadAvatar(InputStream is) throws IOException {
        checkNotNull(is, "is can't be null");

        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        final byte[] jpegStarter = new byte[] { (byte)0xff, (byte)0xd8 };
        byte[] data = new byte[4096];
        int nRead;

        // Skips text content and jumps to image, starting at FF D8.
        // We supposed that it is not between chunks...
        while ((nRead = is.read(data, 0, data.length)) != -1) {
            boolean started = false;

            for (int i = 0; i < nRead - 1; i++) {
                if (data[i] == jpegStarter[0] && data[i + 1] == jpegStarter[1]) {
                    buffer.write(data, i, nRead - i);
                    started = true;
                    break;
                }
            }

            if (started) {
                break;
            }
        }

        // Writes normal content
        while ((nRead = is.read(data, 0, data.length)) != -1) {
            buffer.write(data, 0, nRead);
        }

        buffer.flush();

        avatar = buffer.toByteArray();
    }

    public void deleteAvatar() {
        avatar = new byte[0];
    }

    public boolean isActive() {
        return active;
    }

    public void activate() {
        this.active = true;
    }

    public Date getCreationDate() {
        return creationDate;
    }

    public String login() {
        if (this.sessionId != null) {
            return this.sessionId;
        }

        byte[] hash = Hashing.md5().hashString(getEmail() + new Date().toString()).asBytes();

        checkState(hash.length == 16, "SessionID length missmatch");

        StringBuilder builder = new StringBuilder();

        for (int i = 0; i < hash.length; i++) {
            if (hash[i] < 10) {
                builder.append('0');
            }
            builder.append(Integer.toHexString(hash[i]));
        }

        this.sessionId = builder.toString();

        return this.sessionId;
    }

    public void logout() {
        this.sessionId = null;
    }

    public String getRole() {
        return this.getClass().getSimpleName();
    }

    public void setRole(String role) {
        // Empty: let JSON think he's the boss... or jboss XD
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof User)) return false;

        User user = (User) o;

        if (!email.equals(user.email)) return false;

        return true;
    }

    @Override
    public int hashCode() {
        return email.hashCode();
    }
}
